/* Copyright (c) 2011-2013 ISP RAS, Moscow. For more information please write to
 
      mermakov@ispras.ru
      svartanov@ispras.ru

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package ru.ispras.coffeemachine.instrument;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import org.apache.bcel.generic.ACONST_NULL;
import org.apache.bcel.generic.ALOAD;
import org.apache.bcel.generic.ASTORE;
import org.apache.bcel.generic.BasicType;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.DLOAD;
import org.apache.bcel.generic.DSTORE;
import org.apache.bcel.generic.DUP;
import org.apache.bcel.generic.DUP2;
import org.apache.bcel.generic.F2D;
import org.apache.bcel.generic.FLOAD;
import org.apache.bcel.generic.FSTORE;
import org.apache.bcel.generic.I2L;
import org.apache.bcel.generic.IASTORE;
import org.apache.bcel.generic.ILOAD;
import org.apache.bcel.generic.ISTORE;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.LLOAD;
import org.apache.bcel.generic.LSTORE;
import org.apache.bcel.generic.NEWARRAY;
import org.apache.bcel.generic.NOP;
import org.apache.bcel.generic.PUSH;

import ru.ispras.coffeemachine.target.Daemon;
import ru.ispras.coffeemachine.target.Type;
import ru.ispras.coffeemachine.util.Options;

/**
 * Instruction factory
 * 
 * @author Sergey Vartanov (me@enzet.ru)
 * @author Mikhail Ermakov (mermakov@ispras.ru)
 * @version 1.2.0
 * @since 2011-09-12
 */
public abstract class CoffeeInstructionFactory extends InstructionFactory
{
	private static final long serialVersionUID = 1L;

	protected int invokePrintMethod;

	/**
	 * {@inheritDoc}
	 */
	public CoffeeInstructionFactory(ConstantPoolGen cp)
	{
		super(cp);
	}

	/**
	 * {@inheritDoc}
	 */
	public CoffeeInstructionFactory(ClassGen modifiedCode, ConstantPoolGen cp)
	{
		super(modifiedCode, cp);
	}

	/**
	 * Returns an {@link InstructionList} for simple duplication of stack elements
	 * @param types types of elements on stack top
	 * @param startIndex first unused (within method) JVM register
	 * @return an {@link InstructionList} for simple duplication of stack elements
	 */
	public InstructionList getElements(Type []types, int startIndex) {
		
		if (types.length == 1) {
			if (types[0].getItemSize() == 1) {
				return new InstructionList(new DUP());
			} else {
				return new InstructionList(new DUP2());
			}
		}
		int []indexes = new int[types.length];
		for (int i = 0; i < types.length; i ++) {
			indexes[i] = i;
		}
		
		return getElements(types, indexes, startIndex);
	}
	
	/**
	 * @see CoffeeInstructionFactory#getElements(Type[], int[], InstructionList[], InstructionList[], int)
	 */
	public InstructionList getElements(Type []types, int[] indexes, int startIndex) {
		
		InstructionList []extra = new InstructionList[types.length];
		for (int i = 0; i < types.length; i ++) {
			extra[i] = null;
		}
		
		return getElements(types, indexes, extra, extra, startIndex);
	}
	
	/**
	 * @see CoffeeInstructionFactory#getElements(Type[], int[], InstructionList[], InstructionList[], int)
	 */
	public InstructionList getElements(Type []types, int[] indexes, InstructionList [] after, int startIndex) {
		
		InstructionList [] before = new InstructionList[types.length];
		for (int i = 0; i < types.length; i ++) {
			before[i] = null;
		}
		return getElements(types, indexes, after, before, startIndex);
		
	}

	/**
	 * Duplicate some stack elements into top of stack using registers and
	 * insert some other elements
	 * <p>
	 * For example, if types = {C, B, A}, indexes = {0, 1}, after = {Y1, Y2},
	 * before = {X1, X2}, then
	 * <table>
	 * <tr><th>Stack before</th><th>Stack after</th></tr>
	 * <tr><td>A</td><td>Y2</td></tr>
	 * <tr><td>B</td><td>B</td></tr>
	 * <tr><td>C</td><td>X2</td></tr>
	 * <tr><td>...</td><td>Y1</td></tr>
	 * <tr><td></td><td>C</td></tr>
	 * <tr><td></td><td>X1</td></tr>
	 * <tr><td></td><td>A</td></tr>
	 * <tr><td></td><td>B</td></tr>
	 * <tr><td></td><td>C</td></tr>
	 * <tr><td></td><td>...</td></tr>
	 * </table>
	 * 
	 * @param types element types on top of stack in order to top of stack
	 * @param indexes indexes of elements in order to top of stack
	 * @param after list of instructions that would be placed after
	 *        corresponding duplicated instruction in stack
	 * @param before list of instructions that would be placed before
	 *        corresponding duplicated instruction in stack
	 * @param startIndex index of first register
	 * @return list of instructions provided required actions
	 */
	public InstructionList getElements(Type []types, int[] indexes, InstructionList [] after, InstructionList [] before, int startIndex) {

		int offs = 0;
		int []regIndexes = new int[types.length];
		InstructionList il = new InstructionList();
		for (int i = types.length - 1; i >= 0; i --) {
			regIndexes[i] = startIndex + offs;
			il.append(getStore(types[i], startIndex + offs));
			offs += types[i].getItemSize();
		}
		for (int i = 0; i < types.length; i ++) {
			il.append(getLoad(types[i], regIndexes[i]));
		}
		for (int i = 0; i < indexes.length; i ++) {
			if (before[indexes[i]] != null) {
				il.append(before[indexes[i]]);
			}
			il.append(getLoad(types[indexes[i]], regIndexes[indexes[i]]));
			if (after[indexes[i]] != null) {
				il.append(after[indexes[i]]);
			}
		}
		return il;
	}
	
	/**
	 * Returns a constant push instruction
	 * @param type constant type
	 * @param value constant value
	 * @return a constant push instruction
	 */
	public Instruction pushConstant(Type type, int value) {
		switch (type) {
			case I:
				return new PUSH(cp, value).getInstruction();
			case J:
				return new PUSH(cp, (long)value).getInstruction();
			case F:
				return new PUSH(cp, (float)value).getInstruction();
			case D:
				return new PUSH(cp, (double)value).getInstruction();
			default:
				return new NOP();
		}
	}

	
	/**
	 * Returns a type conversion instruction for type expansion ({@link int} to {@link long} and {@link float} to {@link double})
	 * @param type type to expand
	 * @return a type conversion instruction
	 */
	public Instruction expandPrimitive(Type type) {
		
		
		switch (type) {
			case Z: case S: case C: case I: case B:
				return new I2L();
			case F:
				return new F2D();
			default:
				return new NOP();
		}
	}
	
	/**
	 * Returns an instruction list to initialize an int array
	 * @param array target array
	 * @return instruction list to initialize an int array
	 */
	public InstructionList pushIntArray(int []array) {
		
		InstructionList result = new InstructionList();
		if (array == null) {
			result.append(new ACONST_NULL());
			return result;
		}
		if (array.length == 0) {
			result.append(new ACONST_NULL());
			return result;
		}
		result.append(new PUSH(cp, array.length));
		result.append(new NEWARRAY(BasicType.INT));
		for (int i = 0; i < array.length; i ++) {
			result.append(new DUP());
			result.append(new PUSH(cp, i));
			result.append(new PUSH(cp, array[i]));
			result.append(new IASTORE());
		}
		
		return result;
	}
	
	/**
	 * Duplicate element onto the top of stack
	 * 
	 * @param after list of element types on stack above the target
	 * @param type type of this element
	 */
	public InstructionList getStack(Type[] after, Type type)
	{
		InstructionList il = new InstructionList();

		int start = 200;
		int registerNumber = start;

		for (Type t : after)
		{
			il.append(getStore(t, registerNumber));

			if (t == Type.D || t == Type.J)
			{
				registerNumber++;
				registerNumber++;
			}
			else
			{
				registerNumber++;
			}
		}
		if (type == Type.I || type == Type.F)// || type == Type.A)
		{
			il.append(DUP);
		}
		if (type == Type.D || type == Type.J)
		{
			il.append(DUP2);
		}

		il.append(getStore(type, registerNumber));

		int end = registerNumber;

		for (int i = after.length - 1; i >= 0; i--)
		{
			Type t = after[i];

			if (t == Type.D || t == Type.J)
			{
				registerNumber--;
				registerNumber--;
			}
			else
			{
				registerNumber--;
			}

			il.append(getLoad(t, registerNumber));
		}
		il.append(getLoad(type, end));

		return il;
	}

	/**
	 * Returns an {@link Instruction} for register store
	 * @param type stored value type
	 * @param registerNumber register number
	 * @return an {@link Instruction} for register store
	 */
	public Instruction getStore(Type type, int registerNumber)
	{
		// @formatter:off
		switch (type)
		{
			case Z: case B: case S: case C: case I:
				return new ISTORE(registerNumber);
			case F: return new FSTORE(registerNumber);
			case R: case A: case T:
				return new ASTORE(registerNumber);
			case D: return new DSTORE(registerNumber);
			case J: return new LSTORE(registerNumber);
			default: return null;
		}
		// @formatter:on
	}

	/**
	 * Returns an {@link Instruction} for register load
	 * @param type loaded value type
	 * @param registerNumber register number
	 * @return an {@link Instruction} for register load
	 */
	public Instruction getLoad(Type type, int registerNumber)
	{
		// @formatter:off
		switch (type)
		{
			case Z: case B: case S: case C: case I:
				return new ILOAD(registerNumber);
			case F: return new FLOAD(registerNumber);
			case R: case A: case T:
				return new ALOAD(registerNumber);
			case D: return new DLOAD(registerNumber);
			case J: return new LLOAD(registerNumber);
			default: return null;
		}
		// @formatter:on
	}

	/**
	 * Returns an {@link InstructionList} to replace {@link Daemon#println} method contents
	 * @return an {@link InstructionList} to replace {@link Daemon#println} method contents
	 */
	public abstract InstructionList createDaemonPrintln();
	
	
	/**
	 * Creates an instance of {@link CoffeeInstructionFactory} with requested output strategy (<code>-ifc</code> option)
	 * @param cg BCEL class generator
	 * @param cpg BCEL constant pool generator
	 * @return instruction factory with requested output strategy
	 */
	public static CoffeeInstructionFactory createFactory(ClassGen cg, ConstantPoolGen cpg)
	{
		CoffeeInstructionFactory ifc = null;
		try
		{
			@SuppressWarnings("rawtypes")
			Constructor cont = Class.forName("ru.ispras.coffeemachine.instrument." + Options.instructionFactory + "InstructionFactory").getDeclaredConstructor(ClassGen.class, ConstantPoolGen.class);
			ifc = (CoffeeInstructionFactory) cont.newInstance(cg, cpg);
		}
		catch (NoSuchMethodException e)
		{}
		catch (IllegalArgumentException e)
		{}
		catch (InstantiationException e)
		{}
		catch (IllegalAccessException e)
		{}
		catch (InvocationTargetException e)
		{}
		catch (ClassNotFoundException e)
		{}

		if (ifc == null)
		{
			ifc = new ConsoleInstructionFactory(cg, cpg);
		}
		return ifc;
	}
}
